在上一篇文章中,我们用线程池实现了异步,但是这么做会有两个问题
阻塞队列在内存当中,如果服务器重启会导致任务丢失
只能在单体服务器上运行。若是分布式服务器,则需要进行同步工作,造成额外的开销和风险
所以,现在要采用分布式消息队列来实现异步
什么是消息队列?
MQ(全称Message Queue)是一种进程间通信或同一进程的不同线程间的通信方式,队列就是一个消息容器现实使用中,我们将消息队列称之为中间件,从它的名字就可以看出,消息队列不存储消息内容的本身,它只是消息的搬运工
消息队列的基本模型
消息 Message :一个系统想要向另一个系统传递的数据
生产者 Producer :产生消息 Message 的系统
消费者 Consumer :处理生产者 Producer 产生的消息
队列 Queue :暂时用来存放消息
分布式消息队列的优势是什么?消息队列的优势
异步处理,生产者只需要发送消息就可以了,无需关系消费者何时处理消息
削峰填谷,当消费者的处理能力有限时,而用户的请求量又很大。我们可以先把用户的请求存储在消息队列中,然后消费者或实际执行应用可以按照自身的处理能力逐...
如果采用同步的方法,用户会等很久才能等到系统的响应,这样的体验显然不好于是,我们引入异步,即用户只需要提交任务就好,系统会另开一个新线程去处理这个任务
那么,怎么优雅的实现开新线程这个过程呢?我们会遇到以下问题:
任务队列的最大容量是多少呢?
怎么控制队列长度不超过最大容量呢?
程序如何葱任务队列中取出任务来执行呢?
任务队列的流程如何实现呢?
怎么保证程序最多同时执行多少个任务呢?
。。。。。。?
所以,我们要使用线程池
线程池(Thread Pool) 是一种并发编程中常用的技术,用于管理和重用线程。它由线程池管理器、工作队列和线程池线程组成。线程池的基本概念是,在应用程序启动时创建一定数量的线程,并将它们保存在线程池中。当需要执行任务时,从线程池中获取一个空闲的线程,将任务分配给该线程执行。当任务执行完毕后,线程将返回到线程池,可以被其他任务复用。
为什么需要线程池?
线程的管理复杂 (何时新增线程?何时减少空闲线程?)
任务存取复杂 (何时接受任务?何时拒绝任务?怎么保证大家不会抢到同一个任务?)
线程池的优势
降低资源消耗:通过重复利用已创建的线程降低线程...
限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。限流会导致部分用户请求处理不及时或者被拒,这就影响了用户体验。所以一般需要在系统稳定和用户体验之间平衡一下。
常见的限流算法有四种,分别是:
固定窗口限流算法
滑动窗口限流算法
漏桶算法
令牌桶算法
这里就不挨个展开讲了,感兴趣可以看看这篇文章
使用Redisson实现令牌桶算法来完成限流直接上代码
12345678910111213141516171819public class RedissonLimiterManager { @Resource private RedissonClient redissonClient; /** * 限流操作 * @param key 区分不同的限流器,比如用户id */ public void doRateLimit(String key) { RRateLimiter rateLimiter = redissonClient.getRa...
耗时一个多月,总算结束了黑马点评项目的学习。跟着视频敲代码的同时,也在按照自己的想法去做一些额外的东西,对于Redis的各种用法也算是有了自己的理解。下面,是一些总结笔记吧
项目简介基于Spring Boot + Redis的店铺点评,实现了找店铺 => 秒杀商品 => 写点评 => 看热评 => 点赞关注 => 关注Feed流的完整业务流程。
主要工作
短信登陆:使用Redis实现分布式Session,解决集群间登录态同步问题;使用Redis + Token机制实现单点登录;使用Hash代替String来存储用户信息,节约了 6.7% ,并便于单字段的修改。
店铺查询:使用Redis对高频访问店铺进行缓存,降低DB压力同时提升 89.2% 的数据查询性能;对Redis的所有key设置N + n过期时间,从而合理使用内存并防止缓存雪崩;针对热点店铺缓存,使用逻辑过期机制解决缓存击穿问题,防止数据库宕机;使用缓存Null对象方法,防止缓存穿透的发生,提高了DB容错能力。
缓存的各种问题及解决
为方便其他业务后续使用缓存,使用泛型 + 函数式编程...
什么是UV
UV
全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
PV
全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。
UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中,数据量会非常恐怖。所以,我们需要使用HyperLogLog那么
什么是HyperLogLog
Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。
Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。
指令只有这三条:
PFADD key element [element …]:将任意数量的元素添加到指定的HyperL...
什么是BitMap
所谓 BitMap 就是用一个 bit 位来标记某个元素对应的 value,而 key 即是这个元素。由于采用bit为单位来存储数据,因此在可以大大的节省存储空间。
优点:
效率高,不许进行比较和移位
占用内存少,比如N=10000000;只需占用内存为N/8 = 1250000Bytes = 1.2M,如果采用int数组存储,则需要38M多
缺点:
无法对存在重复的数据进行排序和查找
为什么选择BitMap假如我们用下面这样一张表来存储用户的签到信息
123456789CREATE TABLE `tb_sign` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户id', `year` year NOT NULL COMMENT '签到的年', `month` t...
数据存储商户的地理信息一般用经纬度表示,在这里,我们选择Redis的GEO数据结构进行存储。
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
常用的操作有:
geoadd:添加地理位置的坐标。
geopos:获取地理位置的坐标。
geodist:计算两个位置之间的距离。
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
geohash:返回一个或多个位置对象的 geohash 值。
导入数据店铺Shop在包含的主要信息有
id:店铺id
typeId:店铺类型id
x:经度
y:维度
因为查询涉及到店铺的类型,所以我们按照以下的形式来进行存储:
key:typeId
membeer:id
value:x和y的hash值
代码实现流程
查询店铺
把店铺分组,按照typeId分组,一致的放到一个集合
分批写入Redis123456789101112131415161718192...
什么是Feed流?
关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流和传统模式的区别如下Feed流主要有三种实现方案
拉模式
推模式
推拉结合
拉模式又叫做读扩散,在用户需要读取消息时,主动从发件箱拉取消息,然后把获取到的消息进行排序,并呈现给用户。流程如图:优点:消息只用保存一份,节约内存缺点:大量消息被拉去的话,用户的性能消耗较大
推模式又叫做写扩散,用户发布消息后,就直接推送到粉丝的收件箱里。流程如图:优点:延时低,用户可以很快以较低成本获取到信息缺点:对于内存的负担大,因为一份消息需要被存储很多份
推拉结合模式又叫做读写混合,兼具两种模式的优点。对于普通粉丝,采用拉模式;对于活跃粉丝,采用推模式。如图所示:
三种模式对比
拉模式
推模式
推拉结合
写比例
低
高
中
读比例
高
低
中
用户读取延迟
高
低
低
实现难度
复杂
简单
很复杂
使用场景
很少使用
用户量少、没有大V
过千万的用户量,有大V
基于Redis实现推模式需求
在保存blog到数据库的同时,推送...
为什么要使用ZSET?Redis常用的存储多个数据的数据结构对比
List
Set
SortedSet(ZSET)
排序方式
按添加顺序排序
无法排序
根据score值排序
唯一性
不唯一
唯一
唯一
查找方式
按索引或首位查找
根据元素查找
根据元素查找
查找性能
差
好
好
需求分析
需要实现快速检索:判断当前用户是否已经点过赞
需要实现排序:实现topN
很显然,我们要选择ZSET
如何实现排序?我们需要按照用户点赞时间的先后顺序,取出前n个用户,并在数据库中查找相关信息。所以,我们可以把时间戳作为score来存入ZSET
初步代码实现逻辑很简单,先取出ZSET中前n个数据,然后在数据库中完成查询即可
12345678910111213141516@Overridepublic List<UserDTO> queryByLikes(Long id) { String key = RedisConstants.BLOG_LIKED_KEY + id; // 查询Redis中的top5 Set<St...
目前的点赞功能同一用户可以给同一篇博客多次点赞,且直接与数据库交互,代码如下:
1234567@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) { // 修改点赞数量 blogService.update() .setSql("liked = liked + 1").eq("id", id).update(); return Result.ok();}
希望实现的效果
一个用户只能给一篇博客点赞一次,再次点赞则取消带你赞
如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
实现步骤
给Blog类中添加一个isLike字段,标示是否已经被当前用户点赞
修改点赞功能,利用Redis的set集合判断是否点赞过,是则点赞数+1,否则减一
修改根据id查询Blog的业务,判断当前登录用...